package org.ative.chs;

/**
 * @author James Burkle
 */
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.ative.chs.UciReaderThread;
import org.ative.chs.EngineNotReadyException;

public class UciEngine {

	private int maxWaitTime;
	private OutputStream stdin; 
	private InputStream stdout, stderr;
	private Map<Integer, InputStream> isMap;
	private List<UciOption> uciOptions;
	
	private static BufferedReader engineOut, engineErr;
	private static BufferedWriter engineIn;
	private static Thread engineErrThread;
	private static UciReaderThread engineOutThread;
	
	private static Runtime rt = Runtime.getRuntime();
	private static Process proc;
	private UciInfo uciInfo = new UciInfo();
	private boolean engineIsStopped;
	
	private static Logger logger = Logger.getLogger(UciEngine.class.getName());
	
	public UciEngine (String enginePath, int maxWaitTime) {
		this.maxWaitTime = maxWaitTime;
		
		try {
			// Start engine
			logger.debug("Starting engine at path " + enginePath);
			proc = rt.exec(enginePath);
			// Open up stdin, stdout, and stderr
			stdin = proc.getOutputStream();
	    	stdout = proc.getInputStream();
	    	stderr = proc.getErrorStream();
	    	
	    	engineIn = new BufferedWriter(new OutputStreamWriter(stdin));
	    	engineOut = new BufferedReader(new InputStreamReader(stdout));
	    	engineErr = new BufferedReader(new InputStreamReader(stderr));
	    	
	    	// Start threads to listen to stdout and stderr
	    	engineOutThread = new UciReaderThread(engineOut);
	    	engineOutThread.start();
	    	
	    	engineErrThread = new Thread();
	    	engineErrThread.start();
	    	Thread.sleep(30);
	    	// Initialize engine to UCI mode and get id and options
	    	initUciOptions();
	    	// set the engine to ucinewgame mode
	    	setNewGame();
	    	waitReady();
	    	// get the input stream map for the various info messages
	    	isMap = engineOutThread.getInputStreamMap();
	    	engineIsStopped = true;
	    	// Ready to go
		} catch (IOException e) {
			// TODO Auto-generated catch block
			// add exit logic
			logger.error(e.getMessage());
		} catch (Exception e) {
			// TODO Auto-generated catch block for waitReady
			logger.error(e.getMessage());
		}
	}
	
	/**
	 * 
	 * @param out
	 * @throws IOException
	 */
	private void write(String out) throws IOException {
		engineIn.write(out + "\n");
		engineIn.flush();
	}
	/**
	 * 
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	public void waitReady() throws EngineNotReadyException, IOException {
		write("isready");
		int currWaitTime = 0;
		while (engineOutThread.isReady() == false) {
    		try {
				Thread.sleep(10);
				currWaitTime+=10;
				if (currWaitTime > maxWaitTime){
					throw new EngineNotReadyException();
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				// add exit logic
				logger.error(e.getMessage());
				quit();
			}
    	}
    	engineOutThread.setStatus(false);
	}
	
	/**
	 * 
	 * @param out
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	private void checkedWrite(String out) throws EngineNotReadyException, IOException {
		waitReady();
		write(out);
		waitReady();
	}
	
	/**
	 * 
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	public void stop() throws EngineNotReadyException, IOException {
		write("stop");
		int currWaitTime = 0;
		while (engineOutThread.isStopped() == false) {
    		try {
				Thread.sleep(10);
				currWaitTime+=10;
				if (currWaitTime > maxWaitTime){
					throw new EngineNotReadyException();
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				// add exit logic
				logger.error(e.getMessage());
			}
    	}
		waitReady();
		uciInfo = engineOutThread.getUciInfo();
		engineOutThread.clearUciInfo();
	}
	
	//TODO re-evaluate if this is needed. Maybe just use the bestmove stream in all cases
	public UciBestMove getBestMove() {
		return engineOutThread.getBestMove();
	}
	
	public UciInfo getCurrentInfo() {
		if (!engineIsStopped) {
			this.uciInfo = engineOutThread.getUciInfo();
		}
		return this.uciInfo;
	}
	
	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public int quit() throws IOException {
		write("quit");
		try {
			engineOutThread.interrupt();
			engineOut.close();
			engineIn.close();
			engineErr.close();
			stdin.close();
			stdout.close();
			stderr.close();
		} catch (IOException ignore) {
			// ignore
			ignore.printStackTrace();
		}
		int exit = proc.exitValue();
		logger.debug("Exit value: " + exit);
		//rt.exit(0);
		return exit;
	}
	
	/**
	 * 
	 * @return
	 */
	public InputStream getDepthInputStream() {
		return this.isMap.get(UciStreamNames.DEPTH.ordinal());
	}
	/**
	 * 
	 * @return
	 */
	public InputStream getNpsInputStream() {
		return this.isMap.get(UciStreamNames.NPS.ordinal());
	}
	/**
	 * 
	 * @return
	 */
	public InputStream getPvInputStream() {
		return this.isMap.get(UciStreamNames.PV.ordinal());
	}
	/**
	 * 
	 * @return
	 */
	public InputStream getCurrmoveInputStream() {
		return this.isMap.get(UciStreamNames.CURRMOVE.ordinal());
	}
	/**
	 * 
	 * @return
	 */
	public InputStream getTopEvalInputStream() {
		return this.isMap.get(UciStreamNames.TOPEVAL.ordinal());
	}
	/**
	 * 
	 * @return
	 */
	public InputStream getBestMoveInputStream() {
		return this.isMap.get(UciStreamNames.BESTMOVE.ordinal());
	}
	/**
	 * 
	 * @return
	 */
	public InputStream getNodesInputStream() {
		return this.isMap.get(UciStreamNames.NODES.ordinal());
	}
	
	/**
	 * 
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	public void goInfinite() throws EngineNotReadyException, IOException {
		//search until the "stop" command. Do not exit the search without being told so in this mode!
		waitReady();
		write("go infinite");
	}
	//TODO add go method implementing all of the "go" parameters:
	/* go 	searchmoves <move1> .... <movei>
	 * 		Ponder
	 * 		wtime <x>
	 *			white has x msec left on the clock
	 * 		btime <x>
	 *			black has x msec left on the clock
	 * 		winc <x>
	 *			white increment per move in mseconds if x > 0
	 * 		binc <x>
	 *			black increment per move in mseconds if x > 0
	 * 		movestogo <x>
     * 			there are x moves to the next time control,
	 *			this will only be sent if x > 0,
	 *			if you don't get this and get the wtime and btime it's sudden death
	 */
	/**
	 * <b>setPosition</b><p/>
	 * <code>public void <b>setPosition</b>(String moves)
	 * </code><p/>
	 * <p>Sets up the starting position on internal 
	 * board and plays the moves on the internal chess board.</p>
	 * <p>Note: no "new" command is needed. However, if this position is 
	 * from a different game than the last position sent to the engine, the 
	 * GUI should send a {@link #setNewGame()} in between.</p>
	 * @param moves
	 * @throws EngineNotReadyException
	 * @throws IOException
	 * @see #setNewGame()
	 */
	public void setPosition(String moves) throws EngineNotReadyException, IOException {
		checkedWrite("position startpos moves " + moves);
	}
	/**
	 * <b>setPosition</b><p/>
	 * <code>public void <b>setPosition</b>(String fen, String moves)
	 * </code><p/>
	 * <p>Sets up the position described in the fen string on the internal 
	 * board and plays the moves on the internal chess board.</p>
	 * <p>Note: no "new" command is needed. However, if this position is 
	 * from a different game than the last position sent to the engine, the 
	 * GUI should send a {@link #setNewGame()} in between.</p>
	 * @param fen - A FEN string position indicating the initial 
	 * starting position
	 * @param moves - UCI compatible moves to be played from the FEN
	 * position
	 * @throws EngineNotReadyException
	 * @throws IOException
	 * @see #setNewGame()
	 */
	public void setPosition(String fen, String moves) throws EngineNotReadyException, IOException {
		checkedWrite("position fen " + fen + " moves " + moves);
	}
	/**
	 * <b>setDebug</b><p/>
	 * <code>public void <b>setDebug</b>(boolean debug)</code><p/>
	 * <p>Switches the debug mode of the engine on and off.</p>
	 * In debug mode the engine should send additional infos to the
	 * GUI, e.g. with the "info string" command, to help debugging, 
	 * e.g. the commands that the engine has received etc. This mode 
	 * is be switched off by default and this command can be sent
	 * any time, also when the engine is thinking.
	 * @throws EngineNotReadyException
	 * @throws IOException
	 * @param debug - boolean indicating debug mode on/off
	 */
	public void setDebug(boolean debug) throws EngineNotReadyException, IOException {	
		if (debug) {
			checkedWrite("debug on");
		} else if (!debug) {
			checkedWrite("debug off");
		}
	}
	/**
	 * <b>setNewGame</b><p/>
	 * <code>public void <b>setNewGame</b>()</code><p/>
	 * <p>Notifies the engine that the next search (started with "position" 
	 * and "go" commands) will be from a different game.</p>
	 * <p>This can be a new game the engine should play or a new game it 
	 * should analyze but also the next position from a testsuite with 
	 * positions only.</p>
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	public void setNewGame() throws EngineNotReadyException, IOException {
		checkedWrite("ucinewgame");
	}
	
	/**
	 * 
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	private void initUciOptions() throws EngineNotReadyException, IOException {
		int currWaitTime = 0;
		write("uci");
		while (engineOutThread.isUci() == false) {
			try {
				Thread.sleep(10);
				currWaitTime+=10;
				if (currWaitTime > maxWaitTime){
					throw new EngineNotReadyException();
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				// add exit logic
				logger.error(e.getMessage());
				quit();
			}
		}
		this.uciOptions = engineOutThread.getUciOptions();
	}
	/**
	 * 
	 * @return
	 */
	public List<UciOption> getUciOptions() {
		return this.uciOptions;
	}
	/**
	 * 
	 * @param uciOption
	 * @param optionVal
	 * @throws OptionBoundsException
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	public void setUciOption(UciOption uciOption, String optionVal) 
			throws OptionBoundsException, EngineNotReadyException, IOException {
		//check if there are min/max. If there is, check that the value is in bounds
		if (uciOption.getOptionMin().length() != 0) {
			if (Integer.parseInt(optionVal) < Integer.parseInt(uciOption.getOptionMin())) {
				throw new OptionBoundsException(uciOption, optionVal);
			} else if (Integer.parseInt(optionVal) > Integer.parseInt(uciOption.getOptionMax())) {
				throw new OptionBoundsException(uciOption, optionVal);
			}
		}
		/*
		 *  Should be good, write it. Unfortunately Uci protocol doesn't handshake on options
		 *  so I cant check if the option actually goes through. 
		 */
		checkedWrite("setoption " + uciOption.getOptionName() + " value " + optionVal);
	}
	/**
	 * 
	 * @param uciOption
	 * @throws OptionBoundsException
	 * @throws EngineNotReadyException
	 * @throws IOException
	 */
	public void setUciOption(UciOption uciOption) 
			throws OptionBoundsException, EngineNotReadyException, IOException {
		//check that this is a button type
		if (!uciOption.getOptionType().equals("button")) {
				throw new OptionBoundsException(uciOption);
			} 
		/*
		 *  Should be good, write it. Unfortunately Uci protocol doesn't handshake on options
		 *  so I cant check if the option actually goes through. 
		 */
		checkedWrite("setoption " + uciOption.getOptionName());
	}
	//TODO add register
	//TODO add ponderhit
}